package session

import (
	"fmt"
	"time"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"

	"gitee.com/hasika/gotool/container"
	"gitee.com/hasika/gotool/errorx"

	"gitee.com/hasika/common-server/codec"
	"gitee.com/hasika/common-server/conn/inet"
	"gitee.com/hasika/common-server/frame"
	"gitee.com/hasika/common-server/option"

	"gitee.com/hasika/common-server/pb/sessionpb"
)

type Manager struct {
	serverOption             *option.ServiceOption
	sessionOption            option.ISessionOption
	packetOption             option.IPacketOption
	connectionCallback       inet.ConnectionCallback
	sessionCallback          inet.Callback
	uidToSession             container.MapInterface[uint64, *inet.Session]
	connectionToReaderWriter map[inet.ConnectionType]frame.IFrameReaderWriter
	messageTypeToHandler     map[protoreflect.MessageType]option.MessageHandler
	router                   *option.ProtoMessageRouter
}

func NewManager(serverOption *option.ServiceOption) *Manager {
	m := &Manager{
		serverOption:             serverOption,
		sessionOption:            serverOption.GetSessionOption(),
		connectionCallback:       serverOption.GetConnectionCallback(),
		sessionCallback:          serverOption.GetSessionCallback(),
		packetOption:             serverOption.GetPacketOption(),
		uidToSession:             container.NewSyncMap[uint64, *inet.Session](),
		connectionToReaderWriter: serverOption.GetConnectionToReaderWriter(),
		router:                   option.NewProtoMessageRouter(),
		messageTypeToHandler:     make(map[protoreflect.MessageType]option.MessageHandler),
	}
	serverOption.AddConnectionCallback(m)
	m.initRouter()
	go m.run()
	return m
}

func (m *Manager) initRouter() {
	m.router.RegSimple(&sessionpb.SessionBindReq{}, m.SessionBind, inet.Source, false)
	m.router.RegSimple(&sessionpb.SessionHeartBeatReq{}, m.HeartBeat, inet.Source, false)
	m.router.RegSimple(&sessionpb.SessionUpdateReq{}, m.SessionUpdate, inet.Source, false)
}

func (m *Manager) DeleteSession(uid uint64, reason string) {
	m.deleteSession(uid, reason)
}

func (m *Manager) GetSession(uid uint64) *inet.Session {
	s, _ := m.uidToSession.Load(uid)
	return s
}

func (m *Manager) OnConnectionStart(conn inet.PublicConn) {
}

func (m *Manager) OnConnectionEnd(conn inet.PublicConn) {
	m.deleteConnection(conn, "connection end")
}

func (m *Manager) run() {
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()
	for range ticker.C {
		m.checkExpireSessions()
	}
}

func (m *Manager) checkExpireSessions() {
	now := time.Now()
	m.uidToSession.Range(func(uid uint64, session *inet.Session) (shouldContinue bool) {
		m.checkOneSessionValid(now, session)
		return true
	})
}

func (m *Manager) checkOneSessionValid(now time.Time, session *inet.Session) {
	if now.After(session.GetExpireTime()) {
		inet.Log.Infof("session expired %d, %s", session.Uid, now.String())
		m.deleteSession(session.Uid, "session expired")
	}
	if len(session.GetConnections()) == 0 {
		inet.Log.Infof("session no connection %d, %s", session.Uid, now.String())
		m.deleteSession(session.Uid, "session no connection")
	}
}

func (m *Manager) deleteSession(uid uint64, reason string) {
	session, _ := m.uidToSession.LoadAndDelete(uid)
	if session != nil {
		inet.Log.Infof("delete session %d", uid)
		conns := session.GetConnections()
		for _, c := range conns {
			m.deleteConnection(c, "delete session "+reason)
		}
		m.sessionCallback.OnSessionEnd(session)
	}
	return
}

func (m *Manager) addConnection(uid uint64, conn inet.PublicConn) *inet.Session {
	inet.Log.Infof("add connection %d %+v", uid, conn.GetRemote())
	session, _ := m.uidToSession.Load(uid)
	if session == nil {
		inet.Log.Infof("add session %d", uid)
		newSession := inet.NewSession(uid)
		session, _ = m.uidToSession.LoadOrStore(uid, newSession)
		if newSession == session {
			m.sessionCallback.OnSessionStart(session)
		}
	}
	session.AddConnection(conn)
	return session
}

func (m *Manager) deleteConnection(conn inet.PublicConn, reason string) {
	session := conn.GetSession()
	if session == nil {
		return
	}
	session.RemoveConnection(conn, reason)
	inet.Log.Infof("delete connection %d, %+v", session.Uid, conn.GetRemote())
	m.checkOneSessionValid(time.Now(), session)
}

func (m *Manager) sendToClient(uid uint64, req codec.Packet, replyErr errorx.IError, body proto.Message, conn inet.PublicConn) {
	bs, err := m.packetOption.BuildRspToPacketBytes(uid, req, replyErr, body, nil)
	if err != nil {
		inet.Log.Errorf("build rsp to packet bytes error %s", err)
		return
	}
	rd := m.connectionToReaderWriter[conn.GetConnectionType()]
	err = rd.WriteOnePacketBytes(conn, bs)
	if err != nil {
		inet.Log.Errorf("write packet bytes err %s", err)
		return
	}
}

func (m *Manager) SendToClientByType(uid uint64, nullableReq codec.Packet, replyErr errorx.IError, nullableBody proto.Message, connectionType inet.ConnectionType, sourceCon inet.PublicConn) {
	useCon := m.selectUserConnection(uid, sourceCon, connectionType, true)
	if useCon == nil {
		useCon = sourceCon
	}
	if useCon == nil {
		return
	}
	m.sendToClient(uid, nullableReq, replyErr, nullableBody, useCon)
}

func (m *Manager) RawSendMessageToClient(uid uint64, data []byte, connectionType inet.ConnectionType, autoConnection bool) {
	useCon := m.selectUserConnection(uid, nil, connectionType, autoConnection)
	if useCon == nil {
		return
	}
	rd := m.connectionToReaderWriter[useCon.GetConnectionType()]
	err := rd.WriteOnePacketBytes(useCon, data)
	if err != nil {
		inet.Log.Errorf("uid %d,write raw  bytes err %s,remote %s", uid, err, useCon.GetRemote().String())
		return
	}
}

func (m *Manager) selectUserConnection(uid uint64, sourceCon inet.PublicConn, connectionType inet.ConnectionType, autoConnection bool) inet.PublicConn {
	if connectionType == inet.Source && sourceCon != nil {
		return sourceCon
	}
	session := m.GetSession(uid)
	if session == nil {
		return nil
	}
	useCon := session.GetConnectionByType(connectionType)
	if useCon == nil {
		if autoConnection {
			useCon = session.GetConnectionByType(inet.Tcp)
		}
	}
	if useCon == nil {
		if autoConnection {
			useCon = session.GetConnectionByType(inet.Kcp)
		}
	}
	if useCon == nil {
		return nil
	}
	return useCon
}

func (m *Manager) GetRouter() *option.ProtoMessageRouter {
	return m.router
}

func (m *Manager) SessionBind(data codec.Packet, conn inet.Conn) (errorx.IError, proto.Message) {
	session := conn.GetSession()
	if session != nil {
		return errorx.NewStringErr("session already bind"), nil
	}
	bindReq := data.GetMessage().(*sessionpb.SessionBindReq)
	success, err := m.sessionOption.Bind(bindReq)
	if !success {
		inet.Log.Errorf("session and connection bind failed %+v", err)
		return errorx.NewStringErr(fmt.Sprintf("bind failed %s", err)), nil
	}
	session = m.addConnection(bindReq.Uid, conn)
	session.Uid = bindReq.Uid
	m.sessionOption.PostSessionBindReadWrite(session, bindReq)
	inet.Log.Infof("session bind success %d %d", session.Uid, conn.GetConnectionType())
	return nil, &sessionpb.SessionBindRsp{}
}

func (m *Manager) HeartBeat(data codec.Packet, conn inet.Conn) (errorx.IError, proto.Message) {
	session := conn.GetSession()
	if session == nil {
		return errorx.NewStringErr("session not bind"), nil
	}
	session.KeepAlive()
	m.sessionOption.PostHeartBeatReadOnly(session, data.GetMessage().(*sessionpb.SessionHeartBeatReq))
	//Log.Infof("session keep alive %d %d", session.Uid, conn.GetConnectionType())
	return nil, &sessionpb.SessionHeartBeatRsp{}
}

func (m *Manager) SessionUpdate(data codec.Packet, conn inet.Conn) (errorx.IError, proto.Message) {
	session := conn.GetSession()
	if session == nil {
		return errorx.NewStringErr("session not bind"), nil
	}
	m.sessionOption.PostSessionUpdateReadWrite(session, data.GetMessage().(*sessionpb.SessionUpdateReq))
	return nil, &sessionpb.SessionUpdateRsp{}
}

func (m *Manager) ApplySessionData(data codec.Packet, conn inet.Conn) {
	session := conn.GetSession()
	if session == nil {
		return
	}
	data.SetUid(session.Uid)
	m.sessionOption.ApplySessionCtxReadOnly(session.Ctx, data)
}

func (m *Manager) GetSessions() []*inet.Session {
	ret := make([]*inet.Session, 0, 64)
	m.uidToSession.Range(func(key uint64, value *inet.Session) (shouldContinue bool) {
		ret = append(ret, value)
		return true
	})
	return ret
}
